Wykrywanie dryftu konceptu#

Hint

Do tworzenia wykresów w ramach materiałów została wykorzystana biblioteka plotly. Wykresy generowane przez plotly są statyczne z punktu widzenia przetwarzania strumieni danych, ponieważ wymagają pełnej próbki danych. Wykresy dynamiczne dla danych strumieniowych można byłoby stworzyć za pomocą rozszerzenia plotly - dash dash, natomiast w systemach wdrażanych produkcyjnie popularnym narzędziem do monitoringu danych jest Grafana. W ramach tego laboratorium główny nacisk położony jest na zadaniu wykrywaniu dryftu konceptu danych, dlatego dla uproszczenia wykorzystano statyczne wykresy.

Dane#

W ramach materiałów wygenerowano sztucznie następujące serie danych:

  • Seria danych z dryftem konceptu w zmianie średniej rozkładu, w którym przy próbce 5000 i 10000 zwiększono wartość średniej

  • Seria danych z dryftem konceptu w zmianie odchyleniu standardowym, w którym przy próbce 5000 i 10000 zwiększono wartość odchylenia standardowe rozkładu.

Analiza serii danych z dryftem konceptu w zmianie średniej rozkładu#

Wyświetlanie serii danych#

import pandas as pd
import plotly.express as px


def plot_series(series: pd.DataFrame) -> None:
    fig = px.line(series)
    fig.update_layout(showlegend=False)
    fig.show()


mean_shift_series = pd.read_csv("data/series_with_mean_shift.csv")
plot_series(mean_shift_series)

Seria może przypominać dryft konceptu narastający, jednakże tak jak wspomnienie zostało wcześniej zmiana parametrów rozkładu nastąpiła w punktach 5000 i 10000. Zaznaczmy teraz na wykresie te punkty.

Zaznaczenie punktów zmian parametrów rozkładu#

from typing import List


def plot_series_concept_drift(
    series: pd.DataFrame, detection_points: List[int]
) -> None:
    fig = px.line(series)
    for p in detection_points:
        fig.add_vline(x=p, annotation_text="Concept drift")
    fig.update_layout(showlegend=False)
    fig.show()


plot_series_concept_drift(mean_shift_series, detection_points=[5000, 10000])

Naniesienie punktów zmian na wykres uwidacznia nagłe zmiany w rozkładzie. Zastosujemy teraz metody wykrywania dryft konceptu i porównamy je do rzeczywistych punktów.

Wykrywanie dryftu konceptu w bibliotece river#

Biblioteka river udostępnia następujące metody wykrywania dryftu konceptu

Metoda

Dokumentacja

ADWIN

[DOCS]

DDM

[DOCS]

EDDM

[DOCS]

HDDM_A

[DOCS]

HDDM_W

[DOCS]

KSIWN

[DOCS]

PageHinkley

[DOCS]

W ramach ćwiczenie zastosujemy każdą z metod z domyślnymi parametrami i wyświetlimy znalezione punkty na wykresie

from river.base import DriftDetector
from river.drift import ADWIN, DDM, EDDM, HDDM_A, HDDM_W, KSWIN, PageHinkley
from river.stream import iter_pandas


def detect_concept_drift_from_stream(
    series: pd.DataFrame,
    detector: DriftDetector,
) -> List[int]:
    stream_iter = iter_pandas(series)
    detected_points = []
    for idx, (data, _) in enumerate(stream_iter):
        try:
            detector.update(data["y"])
        except Exception as e:
            print(detector, "Exception: ", e)
            return None
        if detector.drift_detected:
            detected_points.append(idx)
    return detected_points


drift_detectors = {
    "ADWIN": ADWIN(),
    "DDM": DDM(),
    "EDDM": EDDM(),
    "HDDM_A": HDDM_A(),
    "HDDM_W": HDDM_W(),
    "KSWIN": KSWIN(),
    "PageHinkley": PageHinkley(),
}

mean_shift_detected_points = {
    detector_name: detect_concept_drift_from_stream(
        series=mean_shift_series, detector=detector
    )
    for detector_name, detector in drift_detectors.items()
}
DDM Exception:  math domain error

W przypadku implementacji metody DDM w bibliotece river w wersji 0.14 zwracany jest wyjątek math domain error, ponieważ wymaga danych z zakresu 0-1. W przypadku tego algorytmu zastosujemy normalizację serii Min-Max. Poprawmy teraz algorytm wykrywanie dryftu z danych

from river.base import DriftDetector
from river.drift import ADWIN, DDM, EDDM, HDDM_A, HDDM_W, KSWIN, PageHinkley
from river.stream import iter_pandas
from river.preprocessing import MinMaxScaler


def detect_concept_drift_from_stream_v2(
    series: pd.DataFrame, detector: DriftDetector, detector_name: str
) -> List[int]:
    stream_iter = iter_pandas(series)
    detected_points = []
    preprocessor = None
    if detector_name == "DDM":
        preprocessor = MinMaxScaler()
    for idx, (data, _) in enumerate(stream_iter):
        try:
            if preprocessor:
                data = preprocessor.learn_one(data).transform_one(data)

            detector.update(data["y"])

        except Exception as e:
            print(detector, "Exception: ", e)
            return None

        if detector.drift_detected:
            detected_points.append(idx)

    return detected_points


drift_detectors = {
    "ADWIN": ADWIN(),
    "DDM": DDM(),
    "EDDM": EDDM(),
    "HDDM_A": HDDM_A(),
    "HDDM_W": HDDM_W(),
    "KSWIN": KSWIN(),
    "PageHinkley": PageHinkley(),
}

mean_shift_detected_points = {
    detector_name: detect_concept_drift_from_stream_v2(
        series=mean_shift_series, detector=detector, detector_name=detector_name
    )
    for detector_name, detector in drift_detectors.items()
}

Do wyników dodajmy jeszcze nasze “ground truth”, czyli punkty zmian rozkładu.

mean_shift_detected_points["Ground truth"] = [5000, 10000]

Wizualizacja wyników#

from copy import deepcopy
from typing import Dict

import plotly.graph_objects as go
from plotly.subplots import make_subplots


def add_points_to_plot(
    fig: go.Figure, points: List[int], annotation_text: str, row: int, col: int
) -> go.Figure:
    for p in points:
        fig.add_vline(x=p, row=row, col=col)
    return fig


def add_lineplot_to_subplot_fig(
    series: pd.DataFrame,
    fig: go.Figure,
    points: List[int],
    annotation_text: str,
    idx: int,
    num_rows: int,
    num_cols: int,
):
    row, col = np.unravel_index(idx, [num_rows, num_cols])

    fig.add_trace(
        go.Scatter(x=series.index.to_numpy(), y=series["y"]), row=row + 1, col=col + 1
    )
    if not points:
        if isinstance(points, list) == 0:
            fig.layout.annotations[idx].update(
                text=f"{fig.layout.annotations[idx].text} (Exception Occured)"
            )
        else:
            fig.layout.annotations[idx].update(
                text=f"{fig.layout.annotations[idx].text} (No Concept Drift Found)"
            )
        return fig
    
    return add_points_to_plot(
        fig=fig,
        points=points,
        annotation_text=annotation_text,
        row=row + 1,
        col=col + 1,
    )


def plot_detected_points_from_series(
    series: pd.DataFrame,
    detected_points: Dict[str, List[int]],
    num_rows: int,
    num_cols: int,
) -> None:
    assert len(detected_points) == num_rows * num_cols
    assert "Ground truth" in detected_points.keys()
    
    detected_points = deepcopy(detected_points)
    ground_truth = detected_points.pop("Ground truth")
    fig = make_subplots(
        rows=num_rows,
        cols=num_cols,
        subplot_titles=("Ground truth", *tuple(detected_points.keys())),
    )
    fig.update_layout(showlegend=False)

    fig = add_lineplot_to_subplot_fig(
        series=series,
        fig=fig,
        points=ground_truth,
        annotation_text="Concept drift",
        idx=0,
        num_rows=num_rows,
        num_cols=num_cols,
    )
    for detector_id, (detector_name, detector_points) in enumerate(
        detected_points.items()
    ):
        fig = add_lineplot_to_subplot_fig(
            series=series,
            fig=fig,
            points=detector_points,
            annotation_text="Concept drift detected",
            idx=detector_id + 1,
            num_rows=num_rows,
            num_cols=num_cols,
        )
    
    fig.update_layout(
        autosize=False,
        width=800,
        height=1200
    )
    fig.show()


plot_detected_points_from_series(
    series=mean_shift_series,
    detected_points=mean_shift_detected_points,
    num_cols=2,
    num_rows=4,
)

Podsumowanie#

W ramach testowanych algorytmów ADWIN, HDDM_A i PageHinkley na domyślnych parametrach udało się wykryć dwa punkty zmian, które występują blisko rzeczywistych punktom. Najbliżej prawdziwych koordynat był jednak algorytm KSWIN lecz w przypadku tego algorytmu wykryto również dużą liczbę fałszywych punktów zmian.

Analiza serii danych z dryftem konceptu w zmianie odchylenia standardowego rozkładu#

Wyświetlanie serii danych#

std_shift_series = pd.read_csv("data/series_with_std_shift.csv")
plot_series(std_shift_series)

Wykres z zaznaczeniem punktów#

plot_series_concept_drift(std_shift_series, detection_points=[5000, 10000])

W przypadku tego strumienia, zmiany parametrów rozkładu są bardziej widoczne na wykresie ze względu na znaczące zwiększenie się odchylenia standardowego zachowując tę samą średnią. Przetestujmy algorytmy jeszcze raz.

Wykrywanie dryftu konceptu#

drift_detectors = {
    "ADWIN": ADWIN(),
    "DDM": DDM(),
    "EDDM": EDDM(),
    "HDDM_A": HDDM_A(),
    "HDDM_W": HDDM_W(),
    "KSWIN": KSWIN(),
    "PageHinkley": PageHinkley(),
}

std_shift_detected_points = {
    detector_name: detect_concept_drift_from_stream_v2(
        series=std_shift_series, detector=detector, detector_name=detector_name
    )
    for detector_name, detector in drift_detectors.items()
}
std_shift_detected_points["Ground truth"] = [5000, 10000]
plot_detected_points_from_series(
    series=std_shift_series,
    detected_points=std_shift_detected_points,
    num_cols=2,
    num_rows=4,
)

Podsumowanie#

W przypadku serii danych ze zmianami parametrów odchylenia standardowego tylko dla dwóch algorytmów udało się znaleźć punkty zmian przy użyciu domyślnych parametrów. W przypadku metody DDM został znaleziony punkt w punkcie 13000, dla KSWIN znowu udało się znaleźć miejsca zmian, ale również mamy dużo fałszywych punktów. Spróbujmy dostroić metodę KSWIN, w celu ograniczenia liczby wykrytych punktów.

Dostrajanie metody KSWIN#

Zmodyfikujmy parametr alpha metody KSWIN, w celu eliminacji dodatkowego aspektu losowego metody ustawmy parametr ziarna seed na stałą wartość.

drift_detectors = {
    "KSWIN_alpha_0.001": KSWIN(alpha=0.001, seed=441),
    "KSWIN_alpha_0.005": KSWIN(alpha=0.005, seed=441),
    "KSWIN_alpha_0.0001": KSWIN(alpha=0.0001, seed=441),
    "KSWIN_alpha_0.0005": KSWIN(alpha=0.0005, seed=441),
    "KSWIN_alpha_0.00001": KSWIN(alpha=0.00001, seed=441),
}

std_shift_detected_points = {
    detector_name: detect_concept_drift_from_stream_v2(
        series=std_shift_series, detector=detector, detector_name=detector_name
    )
    for detector_name, detector in drift_detectors.items()
}
std_shift_detected_points["Ground truth"] = [5000, 10000]

plot_detected_points_from_series(
    series=std_shift_series,
    detected_points=std_shift_detected_points,
    num_cols=2,
    num_rows=3,
)

W wyniku ręcznej adaptacji parametru alpha udało się ograniczyć liczbę fałszywych punktów, najbliżej rzeczywistości był model KSWIN z parametrem alpha ustawionym na wartość 0.0005.

Podsumowanie#

W materiałach dokonano przeglądu metod do detekcji dryftu konceptu dostępnych w bibliotece river. Do eksperymentów wykorzystano sztucznie przygotowane serie danych. Nawet w takim ustawieniu, gdzie zmiany są na tyle wyraźnie, że możemy je zaobserwować na podstawie wykresu algorytmy detekcji radzą sobie z różnym skutkiem. W przypadku zmian średniej rozkładu, algorytmy bez dodatkowej adaptacji były wstanie znaleźć punkty zmian. Natomiast wykrywanie zmian dotyczących odchylenia standardowego rozkładu jest dużo trudniejszym zadaniem, m. in. ze względu na ograniczenia niektórych metod związanych wyłącznie z analizą średniej.